package br.com.velp.sudecap.daoimpl;

import br.com.velp.sudecap.comum.SpringConstants;
import br.com.velp.sudecap.dao.LogAuditoriaDAO;
import br.com.velp.sudecap.entidade.EntityBase;
import br.com.velp.sudecap.enumerator.TipoOperacaoEnum;
import br.com.velp.sudecap.hibernate.intercept.Auditable;
import br.com.velp.sudecap.hibernate.intercept.Unauditable;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.*;
import java.util.Map.Entry;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import org.hibernate.Criteria;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.transform.Transformers;
import org.hibernate.type.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;

/**
 * <sumario> 
 * <objetivo> "Rastreabilidade" </objetivo> 
 * <autor> EDUARDO SANTOS DE OLIVEIRA </autor> 
 * <versao>1.0.0.0</versao> 
 * <data_criacao> 30.07.2013 </data_criacao> 
 * <data_ultima_alteracao> N/A </data_ultima_alteracao>
 * <requisitos_afetados> N/A </requisitos_afetados> 
 * <descricao> Implementação da Interface Responsável
 *             Pelas Operações de Logs de Auditoria do Sistema 
 * </descricao>
 * </sumario>
 */
@Repository("logAuditoriaDAO")
@Lazy(SpringConstants.LAZY_BEANS)
public class LogAuditoriaDAOImpl implements LogAuditoriaDAO {
    
    public static final Long serialVersionUID = 1l;

    //Constantes Envolvidas na Geração da Consulta SQL
    private static final String IGUAL = " = ";
    private static final String CAMPO_ID = "id";
    private static final String ORDER_BY = " ORDER BY DATA_HORA_OPERACAO DESC ";
    private static final String FROM = " FROM ";
    private static final String WHERE = " WHERE ";
    private static final String VIRGULA = ", ";
    private static final String AS = " AS ";
    private static final String SELECT = " SELECT ";
    private static final String OPERACAO = "CASE OPERACAO WHEN 'U' THEN 'ATUALIZAÇÃO' WHEN 'I' THEN 'INCLUSÃO' ELSE 'EXCLUSÃO' END AS operacao, ";
    private static final String DATA_HORA_OPERACAO = " DATA_HORA_OPERACAO AS dataHora, ";
    private static final String USUARIO_OPERACAO = " USUARIO_OPERACAO AS usuario ";
    
    @Autowired
    private SessionFactory sessionFactory;

    /**
     *
     * Método Responsável Por Obter o Histórico de Modificações Realizadas Em
     * Determinada Entidade
     *
     * @param Long idEntidade
     * @param String nomeTabelaLog
     * @param TipoOperacaoEnum tipoOperacao
     * @param Class resultTransformer
     *
     */
    @Override
    public List recuperarHistoricoModificacoes(Long idEntidade, Class classeBase, TipoOperacaoEnum... tiposOperacao) throws Exception {

        List listaRetorno = new ArrayList();
        List<Map<String, Object>> mapsResultadoPesquisa = criarConsultaLogAuditoria(idEntidade, classeBase, tiposOperacao).list();

        for (Map<String, Object> value : mapsResultadoPesquisa) {
            listaRetorno.add(converterEntidade(value, classeBase));
        }

        return listaRetorno;

    }

    @Override
    public List<Map<String, Object>> recuperarMapaHistoricoModificacoes(Long idEntidade,
            Class classeBase, TipoOperacaoEnum... tiposOperacao) throws Exception {
        
        return criarConsultaLogAuditoria(idEntidade, classeBase, tiposOperacao).list();

    }

    private SQLQuery criarConsultaLogAuditoria(Long idEntidade, Class clazz,
            TipoOperacaoEnum... tipoOperacao) throws NoSuchFieldException {

        Field campoId = clazz.getDeclaredField(CAMPO_ID);
        Field[] fields = clazz.getDeclaredFields();
        
        Map<String, Type> scalares = new HashMap<String, Type>();
        Auditable auditable = (Auditable) clazz.getAnnotation(Auditable.class);
        Column columnId = campoId.getAnnotation(Column.class);

        StringBuilder consulta = new StringBuilder();
        StringBuilder campos = new StringBuilder();

        //Configurando os Campos Padrão Para Exibição das Informações Gerenciais
        //Operação | Data/Hora | Usuário
        campos.append(OPERACAO);
        campos.append(DATA_HORA_OPERACAO);
        campos.append(USUARIO_OPERACAO);
        
        //Configurando os Escalares Para os Campos
        //Padrão Na Consulta
        scalares.put("operacao", StringType.INSTANCE);
        scalares.put("dataHora", DateType.INSTANCE);
        scalares.put("usuario", StringType.INSTANCE);

        //Iniciando a Cláusula SQL Responsável Por Retornar
        //Os Registros de LOG do Sistema
        consulta.append(SELECT);

        //Responsável Por Realizar a Iteração dos Atributos do 
        //Objeto, Adicionando os Nomes das Colunas Como Retorno na
        //Consulta
        for (Field field : fields) {
            adicionarAtributosNaConsulta(field, campos, scalares);
        }

        //Adicionando os Campos de Retorno à Consulta
        consulta.append(campos.toString());

        //Gerando a Cláusula FROM Com Base No Nome da Tabela de LOG
        consulta.append(FROM).append(auditable.logTableName());

        //Adicionando Filtro de Restrição Pela Chave Primária (ID) da Entidade
        consulta.append(WHERE).append(columnId.name()).append(IGUAL).append(idEntidade);

        //Adicionando Filtros de Restrição Pelo Tipo de Operação a Ser Realizada (Insert | Update | Delete)
        if (tipoOperacao != null && tipoOperacao.length > 0) {
            consulta.append(" AND OPERACAO IN (").append(recuperarFiltrosTiposOperacao(tipoOperacao)).append(") ");
        }

        //Ordenando os Resultados de Consulta Pela Data/Hora de Realização da Operação
        consulta.append(ORDER_BY);
        
        SQLQuery sqlQuery = getSession().createSQLQuery(consulta.toString());
        sqlQuery.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);

        //Adicionando Scalares à Consulta Para Compatibilização dos 
        //Tipos do Hibernate Com Os Tipos Retornados do NativeSQL
        for (Entry<String, Type> entry : scalares.entrySet()) {
            sqlQuery.addScalar(entry.getKey(), entry.getValue());
        }

        return sqlQuery;

    }

    private void adicionarAtributosNaConsulta(Field field, StringBuilder campos, Map<String, Type> scalares) {

        //Habilitando o Campo Para Ser Acessado
        field.setAccessible(true);

        //Anotação Que Marca Os Campos Como Não Auditáveis
        Unauditable unauditable = field.getAnnotation(Unauditable.class);

        //Caso a Anotação Exista, Indica Que o Campo é Um Campo Não Auditável, 
        //Portanto Deve Ser Desconsiderado
        if (unauditable != null) {
            return;
        }

        //Capturando as Anotações Responsáveis Por Descrever Os Nomes das
        //Colunas do Banco de Dados
        Column column = field.getAnnotation(Column.class);
        JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);

        //Adicionando Nome da Coluna, Criando ALIAS Para O Nome das Colunas e Adicionando
        //Os Escalares Para Realização da Consulta
        if (column != null) {
            if (!campos.toString().isEmpty()) {
                campos.append(VIRGULA);
            }
            campos.append(column.name()).append(AS).append(field.getName());
            scalares.put(field.getName(), Scalar.getTipoScalar(field.getType()));
        }

        //Adicionando Nome da Coluna, Criando ALIAS Para O Nome das Colunas e Adicionando
        //Os Escalares Para Realização da Consulta
        if (joinColumn != null) {
            if (!campos.toString().isEmpty()) {
                campos.append(VIRGULA);
            }
            campos.append(joinColumn.name()).append(AS).append(field.getName());
            scalares.put(field.getName(), Scalar.getTipoScalar(field.getType()));
        }

    }

    /**
     *
     * Converte o Array de Tipos de Operação Em Um Filtro de Consulta IN / NOT
     * IN
     *
     * @param TipoOperacaoEnum tiposOperacao
     *
     * @return String filtroConsulta
     *
     */
    private String recuperarFiltrosTiposOperacao(TipoOperacaoEnum... tiposOperacao) {
        String filtros = "";
        for (TipoOperacaoEnum tipo : tiposOperacao) {
            if (!filtros.isEmpty()) {
                filtros += VIRGULA;
            }
            filtros += ("'").concat(tipo.getValue()).concat("'");
        }
        return filtros;
    }

    /**
     *
     * Responsável Pela Conversão de Um Mapa(String, Object) Em Uma Determinada
     * Entidade.
     *
     * @param Map<String, Object> mapaEntidade
     * @param Class classeBase
     *
     * @return Object entidadeConvertida
     *
     */
    private Object converterEntidade(Map<String, Object> mapaEntidade, Class classeBase) throws Exception {

        Object newInstance = classeBase.newInstance();

        for (String chave : mapaEntidade.keySet()) {

            Field field = newInstance.getClass().getDeclaredField(chave);
            field.setAccessible(true);
            Object valor = mapaEntidade.get(chave);

            if (valor != null) {
                if (field.getType().getSuperclass().equals(EntityBase.class)) {
                    Object entity = getSession().get(field.getType(), (Serializable) valor);
                    field.set(newInstance, entity);
                } else {
                    field.set(newInstance, valor);
                }
            }

        }

        return newInstance;

    }

    /**
     * Enumerator Responsável Por Descever os Tipos de Scalares Para Tipo de Objeto 
     * (Integer, Number, Float, Double, etc..)
     */
    private enum Scalar {

        INTEGER(Integer.class, IntegerType.INSTANCE),
        STRING(String.class, StringType.INSTANCE),
        DATE(Date.class, DateType.INSTANCE),
        LONG(Long.class, LongType.INSTANCE),
        BOOLEAN(Boolean.class, BooleanType.INSTANCE),
        DOUBLE(Double.class, DoubleType.INSTANCE),
        FLOAT(Float.class, FloatType.INSTANCE),
        NUMBER(Number.class, FloatType.INSTANCE),
        BIG_DECIMAL(BigDecimal.class, BigDecimalType.INSTANCE),
        SHORT(Short.class, ShortType.INSTANCE);
        
        private Class classe;
        private Type hibernateType;

        private Scalar(Class classe, Type hibernateType) {
            this.classe = classe;
            this.hibernateType = hibernateType;
        }

        public Class getClasse() {
            return classe;
        }

        public Type getHibernateType() {
            return hibernateType;
        }

        public static Type getTipoScalar(Class classe) {
            for (Scalar tipo : values()) {
                if (tipo.getClasse().equals(classe)) {
                    return tipo.getHibernateType();
                }
            }
            return LONG.getHibernateType();
        }
        
    }

    @Override
    public String recuperarDescricaoEntidade(Long idEntidade, Class entidade, String atributoDescricao) {

        if (idEntidade == null || entidade == null || atributoDescricao == null) {
            return null;
        }

        Criteria criteria = getSession().createCriteria(entidade);
        criteria.setProjection(Projections.property(atributoDescricao));
        criteria.add(Restrictions.eq(CAMPO_ID, idEntidade));

        return (String) criteria.uniqueResult();

    }

    public Session getSession() {
        return this.sessionFactory.getCurrentSession();
    }
    
}
